package com.example.demo.config;

import com.example.demo.handler.MyAccessDeniedHandler;
import com.example.demo.handler.MyAuthenticationFailureHandler;
import com.example.demo.handler.MyAuthenticationSuccessHandler;
import com.example.demo.handler.myLogoutSuccessHandler;
import com.example.demo.passwordEncoder.MyMD5PasswordEncoder;
import com.example.demo.securitylogin.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

@Configuration
public class MySpringSecurityConfiguration extends WebSecurityConfigurerAdapter {

    /*
     *  重写了父类型中的配置逻辑
     * 如果调用了super.configure(http),则使用默认的流程配置
     * 需要自定义配置，则删除这个方法的调用
     * 当提供了自定义的配置，删除super.configure(http)的时候，所有的默认流程全部清空
     *
     * http.formLogin().loginProcessingUrl(Stirng path)
     *  用户登录请求地址，就是处理用户登录逻辑的地址
     *  springSecurity提供的处理登录请求控制器，是path监听软编码的，可以通过此方法动态配置监听地址
     *  只要配置地址和请求地址一致，即可完成登录逻辑
     *  登录成功，默认返回的地址是‘/‘ ，如果有前置请求，则默认返回前置地址
     *      如：直接访问 loginPage(),登录成功进入 '/'
     *      如：访问 '/ok'，未登录，进入loginPage(),登录成功后，重新饭后 '/ok'
     *  登录失败，默认返回的地址是 http.forLogin().loginPage(String path)方法参数？error
     *
     * @param http 基于http协议的security配置对象，包含所有的security相关配置
     * @author 陈先生
     * @date 2021/4/2 16:41
     * @return void
     * @throws 
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.formLogin()
                .usernameParameter("name")
                .passwordParameter("pwd")
                .loginPage("/login") //当用户未登录时的跳转路径。默认是 ‘/login’
                .loginProcessingUrl("/ok") //用户登录逻辑的请求是什么 ，默认是 ‘/login’
                .successHandler(new MyAuthenticationSuccessHandler("/main",true))//使用自定义的请求处理控制逻辑（Hander）， 实现登录成功后的请求处理
                .failureHandler(new MyAuthenticationFailureHandler("/failure",false)); //使用自定义的请求处理控制逻辑（Hander）， 实现登录失败后的请求处理
//                .failureUrl("/failure");  //用户登录失败后，响应重定向的位置，请求方式是get
//                .failureForwardUrl("/failure"); //用户登录失败后，请求转发的位置，security请求转发使用post请求
//                .defaultSuccessUrl("/main",true) ;//用户登录成功后，响应重定向的的位置，请求方式是get
//                .successForwardUrl("/main"); // 用户登录成功后，请求转发到的位置，security请求转发使用PSOT请求。
        /**
         * 配置退出登录
         */
        http.logout()
                .logoutUrl("/logout")
//                .logoutSuccessUrl("/user/login")
//                .logoutSuccessHandler(new myLogoutSuccessHandler("/login",true))
//                .addLogoutHandler()
                .invalidateHttpSession(true) //回收httpSession对象,退出之前调用HTTPSession.invalidateHttpSession方法
                .clearAuthentication(true); //注销时，清除身份验证



        /**
         * 配置权限校验
         * 权限校验配置是线性的，从开始配置位置开始校验，成功立刻返回，校验失败，继续后续的校验逻辑
         * 如： 请求/login成功。校验.antMatchers成功，授予所有权限，返回。后续的.anyRequest（）不在执行
         * 如： 请求/ok ，校验.antMatchers失败，执行后续的.anyRequest（）,校验，必须等到认证成功之后才能访问
         * 所有的校验失败，都返回到 loginPage()登录页面
         *
         * 授予权限的逻辑。
         * 授予权限的方法，都是基于路径地址匹配基础上调用的
         * 常用方法：
         * permitAll()  - 免登陆模式，任何用户和任意状态都能访问。描述通用资源。如：网站首页 描述静态资源:如css js html文件资源
         * anonymous() - 匿名访问，登录后都不可访问， 如： 登录页面和登录请求
         * authenticated() - 登录后才可以访问  如：订单查看， 密码修改，个人信息
         * denyAll() - 任意用户任意状态都不可访问
         * fullyAuthenticated() - 完整登录才可以访问。 描述敏感资源的：如：钱的操作
         * rememberMe() - 记住我  自动登录|n天免登陆 。描述非敏感资源的。如：除钱相关以外的内容
         * 上述的所有方法，，底层调用的都是统一的方法 access()
         * access（）- 可以实现任何权限授予逻辑，是基于权限表达式的权限授予方法。
         *            表达式规则是由springSecurity定义的
         *
         */
        http.authorizeRequests()
                .antMatchers("/login","/failure").permitAll()
                .antMatchers("/favicon.ico","/select").permitAll()  // '/login'可以任意访问，无需认证
//                基于角色的权限管理
//                .antMatchers("/admin/read").hasRole("管理员")
//                基于资源的权限管理
                .antMatchers("/admin/read").hasAuthority("admin:read")
//                自定义请求的权限管理
                .antMatchers("/admin/writer").access("@myAuthorityPermitImpl.hasAuthority(request,authentication)")
                .anyRequest().authenticated();  //  任何请求都需要校验后才能访问

//        自定义异常访问接口
        http.exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler);
        /*
        * 关闭CSRF协议，为了保证完整流程的可用
        * */
        http.csrf().disable();

        http.rememberMe()
                .rememberMeParameter("remember-me")
                .userDetailsService(userService)
                .tokenRepository(persistentTokenRepository);
    }

    @Autowired
    private UserServiceImpl userService;

    @Autowired
    private PersistentTokenRepository persistentTokenRepository;

    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;
    /**
     *  强散列加密
     * @param
     * @author 陈先生
     * @date 2021/4/2 16:09
     * @return org.springframework.security.crypto.password.PasswordEncoder
     * @throws
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 注入一个持久化用户登录标记Repository对象。
     * JdbcTokenRepositoryImpl --对象，访问数据库Repository对象，需要提供的数据库连接
     * 所有被@Bean描述的方法，其参数，都自动被spring容器中获取对象
     * 否则，抛出异常
     *
     * security持久化用户登录标记，所需要的表格，由security内部构建。
     * 第一次构建的时候，若是没有表格的时候，设置setCreateTableOnStartup（）为true，security框架将内部创建，若存在，则手动关闭
     * @param dataSource
     * @return
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
}
